0.3.0: declare once, call directly — the definition module - #4
Open
bweis wants to merge 4 commits into
Open
Conversation
Question: can one defineWorkflow declaration (workflow + activities + messages + state) make types flow to handler, worker, and client, with the handler engine-agnostic — answering the TypedActivity/DurableDeferred leak? Verdict: YES. The same handler function object runs on (a) a plain in-memory Effect runtime with no engine anywhere and (b) real Temporal via the existing engine, driven identically; payload/success/typed-error inference is pinned end to end; implement() is completeness-checked. Design notes: the single seam is OpsRuntime (six untyped operations the typed ops toolkit dispatches through); primitives are materialized from the declaration (names namespaced by tag); handler R = never. Prototype casts live in makeOps and the temporal runtime — a production version would type the seam the way SandboxHandler was typed. Throwaway. Not for merge; the validated decision informs the 0.3.0 API conversation and the upstream schema'd-activity proposal.
…RDICT: yes) Because every boundary is schema-encoded JSON decoded deterministically on replay, data versioning reduces to: the current schema must decode the wire old code wrote. evolved(current, legacy, migrate) is the declaration-level answer — newest-first union, pure forward migrations, one newest Type for handlers, legacy shapes never re-encoded. Proven through the real wire codec including the V1-history-decodes-under-V2-code case.
Declarations are now callable inside handlers: yield* Charge({ orderId }),
Approval.await, Status.set(...). Every primitive requires only the
WorkflowOps service, the one seam an engine implements, so handlers import
nothing engine-shaped. workflowBundle provides the Temporal runtime;
makeTestWorkflowOps (testing) provides an in-memory one, so the same
handler runs on real Temporal or in a plain unit test.
Also in the definition module: version(site, names) for patch-marker logic
branches and evolved(current, legacy, migrate) for newest-first schema
evolution with pure migrations.
Fixtures, examples, and docs authored with define* throughout. Client-side
driving addresses the declaration's underlying primitive (.update,
.mailbox, .cell, .deferred). Lint: the fork/race versioning rule now also
catches the bare version() call.
From the pre-PR bug/type-honesty audit: - engine-sandbox: eraseR replaces the as-never casts on the WorkflowOps runtime, so only the R channel is erased and success/error shapes stay compile-checked against the seam. UpdateRequest is now an alias of the definition module's (one shape, no drift). temporalWorkflowOps is module-private: outside the per-run wrapper its erased services are missing and every op would die at call time. - definition: WorkflowOpsRuntime.version is generic over the names tuple, deleting three casts across the seam and both engines. Dead deferred cast removed. Documented that declaration schemas must be context-free. - testing: the memory runtime now round-trips EVERY channel through the declaration's wire codec (activity payload/success/error, mailbox and update payloads, update responses, state values, deferred completions), so schema-invalid values defect in unit tests exactly as they would on Temporal. Responding twice to one update dies, mirroring the engine. - lint: the versioning-on-main-fiber rule now recognizes definition-only handler modules (which import nothing engine-shaped) via their version import, alias-aware; the old gate made the bare-version detection dead for exactly the files it was added for. Covered by a new lint fixture.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
0.3.0: the
definitionmodule. Declare each capability once and call it directly inside the handler:Every primitive requires exactly one service,
WorkflowOps— the seam an engine implements.workflowBundleprovides the Temporal runtime; the newmakeTestWorkflowOps(testing module) provides an in-memory one, so the same handler function runs on real Temporal or in a plain unit test with no engine, no sandbox, no server. Handlers import nothing engine-shaped — which is the answer to the "TypedActivity feels like an engine leak" feedback.Also in the module:
version(site, names)(patch-marker logic branches) andevolved(current, legacy, migrate)(newest-first schema evolution with pure migrations). Client-side driving addresses the declaration's underlying primitive:U.update,M.mailbox,C.cell,D.deferred.How it works
Declarations wrap the existing primitives (
TypedActivity,DurableMailbox,DurableUpdate,StateCell, upstreamDurableDeferred) and type each operation against theWorkflowOpscontext service. The seam (WorkflowOpsRuntime) is one operation per primitive kind overunknown; eachdefine*narrows it exactly once, against the schemas the same declaration carries.workflowBundleprovides the Temporal implementation to hosted layers during registration; the per-run wrapper supplies the sandbox services the ops dispatch into. Wire identity stays the explicit name string, so refactors never change the wire.Migration
The whole repo authors with
define*: 16 test fixtures, both examples, all docs (new "Declaring capabilities" guide page), README, EXAMPLES.md. The versioning-chain fixtures deliberately stay on rawActivity.make— they prove replay compatibility with byte-identical commands across code generations, and the full replay drill passes unchanged, which doubles as proof the new plumbing does not alter recorded histories. Low-level per-primitive calls (callActivity,takeMailbox, ...) remain exported fromengine-sandboxas the machinery underneath, undocumented as authoring surface.Audit (second commit)
A dedicated bug/type-honesty audit ran before this PR; its findings are the second commit:
eraseRreplaces theas nevercasts on the Temporal ops runtime — only the R channel is erased; success/error shapes stay compile-checked against the seam.UpdateRequestunified to one shape.temporalWorkflowOpsmade module-private (outside the per-run wrapper it would die at call time).WorkflowOpsRuntime.versionis generic over the names tuple: three casts deleted, both engines implement it verbatim.versioning-on-main-fiberlint rule now covers definition-only handler modules via theirversionimport (alias-aware); the old import gate made the new detection dead for exactly those files.Validation
definition.test.ts(same handler on memory + real Temporal, typed update refusal, typed activity failure) andschema-evolution.test.ts